/* Copyright © 2023 - 2024 Coremail论客
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import buffer from '@ohos.buffer';
import util from '@ohos.util';
import { getLogger } from './log';
import { CRLF, sCRLF, transformStream } from "./common";
import { IBuffer, IBufferCreator } from '../api';
import { findBuffer, joinBuffers, removeBufferData } from './buffer_utils';
import { memBufferCreator } from './file_stream';

const logger = getLogger('encoding');

function createDecoder(charset?: string, options?: util.TextDecoderOptions): util.TextDecoder {
  if (!charset) {
    charset = 'utf-8';
  } else { // gb2312 or gbk
    charset = charset.toLowerCase()
    if (charset.startsWith('gb')) {
      charset = 'gb18030';
    }
  }

  const decoder = util.TextDecoder.create(charset, options);
  return decoder;
}

const decoders = ['gb18030', 'utf-8', 'latin1'].map(
  charset => util.TextDecoder.create(charset, {
    fatal: true
  }));

export function decodeCharset(
  buff: Uint8Array,
  charset: string = "utf-8"
): string {
  if (buff.byteLength == 0) {
    return "";
  }
  const decoder = createDecoder(charset, {
    fatal: true
  });
  let s = '';
  try {
    s = decoder.decodeWithStream(buff, { stream: false })
  } catch(e) {
    for (const decoder of decoders) {
      try {
        s = decoder.decodeWithStream(buff, { stream: false });
        break;
      } catch(e) {
        logger.debug(`decode charset failed`);
      }
    }
  }
  return s;
}

export async function decodeCharsetStream(
  buff: IBuffer,
  charset: string = "utf-8"
): Promise<string> {
  let result: string[] = [];
  let decoder = createDecoder(charset);
  const func = async (decoder, buff) => {
    const result: string[] = [];
    for await (const chunk of buff.readRaw()) {
      const s = decoder.decodeWithStream(chunk, { stream: true });
      if (s.length) {
        result.push(s);
      }
    }
    return result;
  }
  try {
    result = await func(decoder, buff)
  } catch(e) {
    for (const decoder of decoders) {
      try {
        result = await func(decoder, buff)
        break;
      } catch(e) {
        logger.debug(`decode charset failed`);
      }
    }
  }
  return result.join('');
}

export function encodeUtf8(s: string): Uint8Array {
  return new util.TextEncoder().encodeInto(s);
}

export function base64Encode(
  str: string | Uint8Array,
  maxPerLine = 76,
  charset: buffer.BufferEncoding = "utf-8"
): string {
  if (!str) {
    return '';
  }
  let buff =
    typeof str == "string" ? buffer.from(str, charset) : buffer.from(str);
  let text = buff.toString("base64");
  if (maxPerLine > 0 && maxPerLine % 4 != 0) {
    maxPerLine -= maxPerLine % 4;
  }
  if (maxPerLine > 0 && text.length > maxPerLine) {
    const ss = [];
    let s = 0;
    let e = s + maxPerLine;
    while (e < text.length) {
      ss.push(text.substring(s, e));
      s = e;
      e = s + maxPerLine;
    }
    ss.push(text.substring(s));
    text = ss.join(sCRLF);
  }
  return text;
}

export function base64Decode(
  str: string,
  charset: buffer.BufferEncoding = "utf-8"
): Uint8Array {
  const buff = buffer.from(str, 'base64');
  return new Uint8Array(buff.buffer)
}

// quoted-printable
export function qpEncode(
  str: string | Uint8Array,
  encodeSpace: boolean = true,
): string {
  let buff: Uint8Array;
  if (typeof str !== "string") {
    buff = str;
  } else {
    const enc = new util.TextEncoder();
    buff = enc.encodeInto(str);
  }
  let res: string[] = [];
  let h = 0, l = 0;
  const DexChars = '0123456789ABCDEF'
  for (const v of buff) {
    if (encodeSpace && v == 0x20) { // 空格编码成下划线
      res.push('_');
    } else if (qpNeedEncode(v) || (encodeSpace && v == 0x5F)) { // 下划线需要编码
      h = v >>> 4;
      l = v & 0x0F;
      res.push('=', DexChars[h], DexChars[l]);
    } else {
      res.push(String.fromCharCode(v));
    }
  }
  const s = res.join('');
  return s;
}

export function qpDecode(str: string, decodeUnderline = true): Uint8Array {
  const buff = new Uint8Array(str.length);
  let index = 0;
  for (let i = 0; i < str.length; i++) {
    if (str[i] != "=") {
      if (decodeUnderline && str[i] == '_') { // 下划线表示空格
        buff[index] == 0x20;
      } else {
        buff[index] = str.charCodeAt(i);
      }
      index += 1;
    } else if (i <= str.length - 3) {
      const s = str.substring(i + 1, i + 3);
      if (s != "\r\n") {
        buff[index] = parseInt(s, 16);
        index += 1;
      }
      i += 2;
    } else {
      logger.warn('qp decode error', str.length, str.substring(0, 20), decodeUnderline);
      // error
    }
  }
  const v = new Uint8Array(buff.buffer, 0, index);
  return v;
}

export function getEncodeMethod(s: string, qpThreshold = 0.7): "B" | "Q" | "" {
  if (qpThreshold == 0) {
    return 'B';
  } else if (qpThreshold == 1) {
    return 'Q';
  }
  const nonAscii = s.replace(/[\x20-\x7e]+/g, '')
  if (nonAscii.length == 0) {
    return '';
  }
  const noEncodeNumber = s.length - nonAscii.length;
  if (noEncodeNumber > s.length * qpThreshold) {
    return "Q";
  } else {
    return "B";
  }
}

function qpNeedEncode(ch: number) {
  return (ch < 0x20 || ch > 0x7E || ch == 0x3D);
}

export function isAsciiPrintable(s: string): boolean {
  s = s.replace(/[\x20-\x7e]+/g, '')
  return s.length == 0;
}

export function encodeMimeString(s: string, qpThreshold = 0.7, encodeSpace = false): string {
  const m = getEncodeMethod(s, qpThreshold);
  if (m == "") {
    return s;
  }
  const useQP = m == "Q";
  const prefix = useQP ? "=?UTF-8?Q?" : "=?UTF-8?B?";
  const MAX_CHARS_PER_LINE = 50;
  const res = [];
  for (let i = 0; i < s.length; i += MAX_CHARS_PER_LINE) {
    const ss = s.substring(i, i + MAX_CHARS_PER_LINE);
    const r = useQP ? qpEncode(ss, encodeSpace) : base64Encode(ss, -1);
    res.push(`${prefix}${r}?=`);
  }
  const result = res.join('\r\n\t');
  return result;
}

export function decodeMimeString(s: string, decodeUnderline = true): string {
  let decoder: util.TextDecoder;
  return s.replace(/=\?([\w-]+)\?([QBqb])\?([\x20-\x7e]+?)\?=/g,
      (s: string, charset: string, encoding: string, data: string) => {
    if (!decoder) {
      decoder = createDecoder(charset);
    }
    encoding = encoding.toUpperCase();
    let buff: Uint8Array;
    if (encoding == "B") {
      buff = base64Decode(data);
    } else /* if (encoding == "Q")*/ {
      buff = qpDecode(data, decodeUnderline);
    }
    const r = decoder.decodeWithStream(buff, { stream: true });
    return r;
  })
}

export function decodeUtf7(s: string): string {
  return s.replace(/&([^-]*)-/g, (m: string, p1: string) => {
    if (p1 == "") {
      return "&";
    } else {
      const s = p1.replace(/,/g, "/").replace(/&-/g, "&");
      const b = base64Decode(s);
      const ss = [];
      for (let i = 0; i < b.byteLength; i += 2) {
        ss.push(String.fromCharCode((b[i] << 8) | b[i + 1]));
      }
      return ss.join("");
    }
  });
}

export function encodeUtf7(s: string): string {
  return s.replace(/&/g, "&-").replace(/[^\x20-\x7e]+/g, m => {
    const b = new Uint8Array(m.length * 2);
    for (let i = 0; i < m.length; i++) {
      const v = m.charCodeAt(i);
      b[i * 2] = v >> 8;
      b[i * 2 + 1] = v & 0xff;
    }
    return "&" + base64Encode(b).replace(/\//g, ",").replace(/=+$/, "") + "-";
  });
}

export async function base64EncodeStream(
  inputStream: IBuffer,
  bufferCreator: IBufferCreator,
): Promise<IBuffer> {
  const base64 = new util.Base64Helper();
  const res = bufferCreator.createBuffer({});
  let buffer: Uint8Array | undefined;
  let remain: Uint8Array | undefined;
  for await (const buff of inputStream.readRaw()) {
    if (buffer) {
      buffer = joinBuffers([buffer, buff]);
    } else {
      buffer = buff;
    }
    // 每行57个字节，编码成76个字符
    if (buffer.byteLength < 57) {
      continue;
    }
    const remainByteNumber = buffer.byteLength % 57;
    if (remainByteNumber > 0) {
      const encodeLength = buffer.byteLength - remainByteNumber;
      remain = new Uint8Array(remainByteNumber)
      remain.set(buffer.subarray(encodeLength));
      buffer = buffer.subarray(0, encodeLength);
    }
    let result = await base64.encodeToString(buffer);
    res.feed(result.replace(/.{76}/g, m => m + '\r\n'));
    buffer = remain;
    remain = undefined;
  }
  if (buffer) {
    let result = await base64.encodeToString(buffer);
    res.feed(result);
  }
  res.end();
  return res;
}

export async function base64DecodeStream(
  inputStream: IBuffer,
  bufferCreator: IBufferCreator
): Promise<IBuffer> {
  const base64 = new util.Base64Helper();
  const res = bufferCreator.createBuffer({});
  const LF = new Uint8Array([10]);
  let buff = new Uint8Array(0);
  logger.debug("base64DecodeStream start", await inputStream.getSize())
  for await (let chunk of inputStream.readRaw()) {
    logger.debug("base64DecodeStream u8a", buff.byteLength, chunk.byteLength)
    if (buff.byteLength > 0) {
      buff = joinBuffers([buff, chunk]);
    }
    else {
      buff = chunk;
    }
    buff = removeBufferData(buff, CRLF);
    buff = removeBufferData(buff, LF);
    if (buff.byteLength < 4) {
      continue;
    }
    const remain = buff.byteLength % 4;
    let remainBuff = new Uint8Array(0);
    if (remain > 0) {
      remainBuff = buff.subarray(buff.byteLength - remain);
      buff = buff.subarray(0, buff.byteLength - remain);
    }
    logger.debug("base64DecodeStream u8a", buff.byteLength, remain)
    const u8a = await base64.decode(buff, util.Type.BASIC);
    res.feed(u8a);
    buff = remainBuff;
  }
  if (buff.byteLength > 0 && buff.byteLength % 4 == 0) {
    logger.debug("base64DecodeStream u8a", buff.byteLength)
    const u8a = await base64.decode(buff, util.Type.BASIC);
    res.feed(u8a);
  }
  res.end();
  logger.debug("base64DecodeStream end")
  return res;
}

export async function qpDecodeStream(
  inputStream: IBuffer,
  bufferCreator: IBufferCreator = memBufferCreator
): Promise<IBuffer> {
  const res = bufferCreator.createBuffer({});
  let buff: Uint8Array = new Uint8Array(0);
  logger.debug("qpDecodeStream start")
  for await (let chunk of inputStream.readRaw()) {
    logger.debug("qpDecodeStream u8a", buff.byteLength, chunk.byteLength)
    if (buff.byteLength > 0) {
      buff = joinBuffers([buff, chunk]);
    }
    else {
      buff = chunk;
    }
    if (buff.byteLength < 3) {
      continue;
    }
    let l = buff.byteLength - 1;
    if (buff[l] == 0x3D) { // ends with '='
      l--;
    } else if (buff[l - 1] == 0x3D) { // ends with '=.'
      l -= 2;
    }
    const b = new Uint8Array(buff.buffer, buff.byteOffset, l + 1);
    res.feed(qpDecode(decodeCharset(b), false));
    if (l < buff.byteLength - 1) {
      buff = buff.subarray(l + 1);
    } else {
      buff = new Uint8Array(0);
    }
  }
  if (buff.byteLength) {
    res.feed(qpDecode(decodeCharset(buff), false));
  }
  res.end();
  logger.debug("qpDecodeStream end")
  return res;
}

export function decodeParameterString(s: string) {
  const [charset, lang, ...data] = s.split("'");
  const ss = data.join("'").replace(/%([0-9A-F]{2})/g, (all, match) => {
    const d = parseInt(match, 16);
    return String.fromCharCode(d);
  });
  const bytes = new Uint8Array(ss.split('').map(s => s.charCodeAt(0)));
  const t = decodeCharset(bytes, charset);
  return t;
}
